using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.IO;
using Microsoft.Win32;

namespace PDO.Utilities
{
    /// <summary>
    /// Generic base class for Plugin functionality
    /// </summary>
    /// <typeparam name="T">The attribute type that this plugin manager class will look for</typeparam>
    public class PluginManagerBase<T>
    {
        public const string PLUGIN_SEARCH_PATTERN = "*.plugin.dll";

        public class PluginInformation<T>
        {
            public T PluginAttribute { get; set; }
            public Type PluginImplmentationType { get; set; }
        }

        protected List<PluginInformation<T>> _plugins;

        public virtual Type EnforcedBaseClass { get; private set; }

        public PluginManagerBase()
        {
            if (_plugins == null)
            {
                _plugins = new List<PluginManagerBase<T>.PluginInformation<T>>();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="enforcedBaseClass">The baseclass that will be enforced on loaded types</param>
        public PluginManagerBase(Type enforcedBaseClass)
            : this()
        {
            EnforcedBaseClass = enforcedBaseClass;
        }

        /// <summary>
        /// Loads plugins of type <typeparamref name="T"/> from the current calling assembly
        /// </summary>
        public void LoadInternalSources()
        {          
            // Load the core plugins from the calling assembly
            LoadPlugin(new FileSystemItem(Assembly.GetCallingAssembly().Location));
        }

        /// <summary>
        /// Loads services from a external filesystem item
        /// </summary>
        /// <param name="item"></param>
        public void LoadExternalSources(FileSystemItem item)
        {
            LoadPlugin(item);
        }

        /// <summary>
        /// Sets the default location to load plugins from.
        /// </summary>
        public virtual string PluginsDirectory
        {
            get
            {                
                string assemblyPath = AppDomain.CurrentDomain.BaseDirectory;
                return Path.Combine(assemblyPath, "Plugins");
            }
        }


        /// <summary>
        /// Load plugins from the default plugin source directory
        /// </summary>
        /// <exception cref="FileNotFoundException"/>
        public virtual void LoadFromPluginSource()
        {
            // Verify the Path's validity
            if (!Directory.Exists(PluginsDirectory))
            {
                throw new FileNotFoundException("Plugins directory does not exist.", PluginsDirectory);
            }

            string[] files = System.IO.Directory.GetFiles(PluginsDirectory, PLUGIN_SEARCH_PATTERN, SearchOption.AllDirectories);
            foreach (string file in files)
            {
                LoadExternalSources(new FileSystemItem(file));
            }
        }        

        private void LoadPlugin(FileSystemItem item)
        {
            System.Type[] types;
            System.Reflection.Assembly assembly = null;
            System.Reflection.Module[] moduleArray = null;

            // Load the assembly specified.
            try
            {
                assembly = System.Reflection.Assembly.LoadFrom(item.AbsolutePath);
            }
            catch (BadImageFormatException ex)
            {
                // Non .Net assemby was found
                Logger.WriteError(string.Format("Non .NET assembly skipped during loading process. FileName {0} Full Details {1}", item.AbsolutePath, ex.ToString()));
                return;
            }

            if (assembly == null)
            {
                throw new InvalidOperationException(item.AbsolutePath);
            }


            moduleArray = assembly.GetModules();

            /// Iterate through each module found in the assembly. Within each module, find all 
            /// items that implement the Attribute.         

            foreach (System.Reflection.Module module in moduleArray)
            {
                types = new Type[0];
                try
                {
                    types = module.GetTypes();
                }
                catch (ReflectionTypeLoadException ex)
                {                    
                    StringBuilder loaderExceptions = new StringBuilder("Loader Exceptions: ");
                    foreach (Exception lex in ex.LoaderExceptions)
                    {
                        loaderExceptions.AppendLine(lex.ToString());
                    }
                    Logger.WriteError(string.Format("Failed to load module type {2} during loading process. FileName {0} Full Details {1} {3}", item.AbsolutePath, ex.ToString(), module.Name, loaderExceptions.ToString()));
                }


                foreach (Type moduleType in types)
                {
                    T attrib = default(T);
                    
                        attrib = ValidateLoadedPlugin(moduleType);
                    

                    if (attrib == null)
                        continue;

                    //Enforce the base class constraint        
                    if (EnforcedBaseClass != null)
                    {
                        if (!EnforcedBaseClass.IsAssignableFrom(moduleType))
                            continue;
                    }

                    bool addToInternalList = true;
                    //alert inherited classes
                    OnValidatePlugin(attrib, moduleType, ref addToInternalList);

                    if (addToInternalList)
                    {
                        AddPlugin(attrib, moduleType);
                    }
                }
            }
        }


        /// <summary>
        /// Enforce rules that must be followed with the ServiceClientPlugin attribute
        /// </summary>
        /// <param name="moduleType"></param>
        /// <returns>null if the type does not meet all specifications</returns>
        private T ValidateLoadedPlugin(Type moduleType)
        {
            T attrib;

            attrib = GetClassAttribute(moduleType);

            if (attrib == null)
                return attrib;

            return attrib;
        }

        /// <summary>
        /// Override to preform manual operations on the plugin once it has been identified by the framework
        /// </summary>
        /// <param name="attrib"></param>
        /// <param name="addToInternalList">Set to false to preclude this item from being added to the internal plugin list</param>
        /// <returns></returns>
        /// <example>
        /// Example of enforce default constructor rule for instanciation
        /// Type[] empty = new Type[0];
        /// ConstructorInfo ci = moduleType.GetConstructor(new Type[0]);
        /// if (ci == null)
        /// {
        ///     throw new ArgumentException("The plugin must contain a parameterless constructor");
        /// }
        /// </example>
        protected virtual void OnValidatePlugin(T attrib, Type moduleType, ref bool addToInternalList)
        {
        }

        /// <summary>
        /// Adds the managed service to the interal list
        /// </summary>
        /// <param name="service"></param>
        private void AddPlugin(T attrib, Type attachedObject)
        {
            _plugins.Add(new PluginManagerBase<T>.PluginInformation<T>()
            {
                PluginAttribute = attrib,
                PluginImplmentationType = attachedObject
            });

        }

        /// <summary>
        /// Get the attribute of type T associated with a class type
        /// </summary>
        /// <typeparam name="T">Class Type</typeparam>
        /// <param name="t"></param>
        /// <returns>null if the attribute is not found or the type is not a class.</returns>
        public T GetClassAttribute(Type t)
        {
            if (!t.IsClass)
            {
                return default(T);
            }
            return GetAttribute(t);
        }

        /// <summary>
        /// Get the attribute of type T
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns>null if the attribute is not found.</returns>
        private T GetAttribute(Type t)
        {
            object[] attributes = t.GetCustomAttributes(typeof(T), true);

            if (attributes.Length < 1)
            {
                return default(T);
            }

            object tempAttrib = attributes[0];

            T attribute = (T)tempAttrib;

            return attribute;
        }
    }
}
